[JavaScript] Curry

前言

Curry化指的是將接受多個參數的函數轉換成可以依次傳入參數的函式,以下見例子。

1
2
3
4
5
var add = function(x , y){
return x + y ;
}
add(1,2) ; // 3

Curry 化後

1
2
3
4
5
6
var add = function(x){
return function(y){
return x + y ;
}
}
add(1)(2); // 3

這樣做有什麼好處?好處在於我們利用不同的參數來創造不同的函式。

1
2
3
4
var addOne = add(1) ;
var addTwo = add(2) ;
addOne(1) ; // 2
addTwo(1) ; // 3

許多函式庫有提供Curry化的功能,像是lodash等等的函式庫。

1
2
3
4
5
6
7
8
var add = function(x , y , z){
return x + y + z ;
}
var curryAdd = _.curry(add) ;
curryAdd(1,2,3); // 6
curryAdd(1,2)(3); // 6
curryAdd(1)(2)(3); // 6

接下來透過實作來了解是如何辦到 Curry 功能的。

實作

首先無論如何先回傳一個函式

1
2
3
4
5
var curry = function(fn){
return function(){
}
}

再來我們要知道什麼時候是參數已經傳完的情況並且作處理,若參數已經傳完則利用 apply 執行函式。

在這邊先 slice 一份 arguments,而由於 arguments 不是陣列而是物件,因此不可以直接對 arguments 做 slice 。

1
2
3
4
5
6
7
8
var curry = function(fn){
return function(){
var args = Array.prototype.slice.call(arguments, 0);
if ( args.length >= fn.length ) {
return fn.apply(null, args);
}
}
}

若參數還沒傳完,這時候要回傳個函式讓使用者繼續呼叫

1
2
3
4
5
6
7
8
9
10
11
12
var curry = function(fn){
return function(){
var args = Array.prototype.slice.call(arguments, 0);
if ( args.length >= fn.length ) {
return fn.apply(null, args);
} else {
return function() {
}
}
}
}

若再次呼叫這個函式,則直接把帶入的參數連接到之前的參數後面,並再次呼叫原先的 curried 函式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var curry = function(fn){
// 為了再次呼叫命名為 curried
return function curried(){
var args = Array.prototype.slice.call(arguments, 0);
if ( args.length >= fn.length ) {
return fn.apply(null, args);
}
else {
return function() {
var args2 = Array.prototype.slice.call(arguments, 0);
return curried.apply(null, args.concat(args2));
}
}
}
}

如此一來就完成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var curry = function(fn){
return function curried(){
var args = Array.prototype.slice.call(arguments, 0);
if ( args.length >= fn.length ) {
return fn.apply(null, args);
} else {
return function() {
var args2 = Array.prototype.slice.call(arguments, 0);
return curried.apply(null, args.concat(args2));
}
}
}
}
var add = function(x , y ,z){
return x + y + z ;
}
var add = curry(add);
console.log(add(1,2)(3)); // 6
console.log(add(1)(2,3)); // 6
console.log(add(1)(2)(3)); // 6
console.log(add(1,2,3)); // 6

不限參數

若我們希望這個函式不限定參數數量

1
2
3
add(1) //1
add(1)(2) // 3
add(1)(2)(3)// 6

以上的方法是不合理的,因為它不知道現在該繼續回傳函式還是該回傳結果,我們可以提供個函式讓它知道已經執行完要得到結果。

1
2
3
add(1).value() //1
add(1)(2).value() // 3
add(1)(2)(3).value()// 6

首先一樣先回傳一個函式,並且讓這個函式回傳自己以便繼續傳入參數,且另外給這個函式一個 value 函式。

1
2
3
4
5
6
7
8
9
var add = function(){
var curried = function(){
return curried ;
}
curried.value = function(){
}
return curried ;
}

在每次帶入參數後將值加總,並在 value 函式回傳結果即可。

1
2
3
4
5
6
7
8
9
10
var add = function(sum){
var curried = function(num){
sum += num ;
return curried ;
}
curried.value = function(){
return sum ;
}
return curried ;
}

如此一來即可不斷對 add 帶入參數

1
2
3
4
5
6
var myAdder = add ;
for( var i = 0; i < 10; i ++) {
myAdder = myAdder(10);
}
myAdder(10); // 100

利用 arguments 來處理一次帶入多個參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var add = function(){
var sum = 0 ;
for ( var i = 0 ; i < arguments.length ; i ++ ){
sum += arguments[i] ;
}
var curried = function(){
for ( var i = 0 ; i < arguments.length ; i ++ ){
sum += arguments[i] ;
}
return curried ;
}
curried.value = function(){
return sum ;
}
return curried ;
}
add(1).value() //1
add(1)(2).value() // 3
add(1)(2)(3).value() // 6
add(1,2,3)(4).value() // 10
add(1)(2,3,4).value() // 10

參考

Gettin’ Freaky Functional w/Curried JavaScript